package com.albedo.java.common.config;

import cn.hutool.core.util.ArrayUtil;
import com.albedo.java.common.core.annotation.AnonymousAccess;
import com.albedo.java.common.core.config.ApplicationProperties;
import com.albedo.java.common.core.constant.CommonConstants;
import com.albedo.java.common.core.constant.SecurityConstants;
import com.albedo.java.common.security.component.Http401UnauthorizedEntryPoint;
import com.albedo.java.common.security.enums.RequestMethodEnum;
import com.albedo.java.common.security.jwt.JwtConfigurer;
import com.albedo.java.common.security.jwt.TokenProvider;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.PostConstruct;
import java.util.*;


/**
 * @author somewhere
 * @description
 * @date 2020/5/31 17:11
 */

/**
 * Security 有两种认证方式：
 *    1. httpbasic
 *    2. formLogin
 * 同样，Security 也提供两种过滤器类：
 *   UsernamePasswordAuthenticationFilter 表示表单登陆过滤器
 *   BasicAuthenticationFilter 表示 httpbaic 方式登陆过滤器
 *
 */


//HttpserveletResponse#resetBuff()      //重置缓冲区的数据，不重置 返回状态码和 相应头headers
//HttpserveletResponse#isCommitted()   //判断是否提交

/**
 *  UserDetails :  认证登录 成功后返回的对象，  这个对象 包含 该用户的权限。 一般我们都会 继承该对象。
 *  Authentication: 认证对象，常用的实现类是：UsernamePasswordAuthenticationToken， 封装了username/password
 *
 *
 *  登录的主要流程：   UsernamePasswordAuthenticationFilter 中的doFilter() 方法：
 *   1. 验证当前请求是否是登录请求，不是执行doFilter。
 *   2. 尝试登录，从request 中获取username/password封装成UsernamePasswordAuthenticationToken对象
 *      在由AuthenticationManager对象执行authenticate（）方法。
 *      ps:这里AuthennticationManager的实现是  ProviderManager
 *
 *   3. ProviderManager获取所有的AuthenticationProvider对象，遍历。 找到支持的authenticate（）的对象。
 *
 *   4. 一般使用DaoAuthenticationProvider对象， 该对象会调用retrieveUser（）---》getUserDetailsService().loadUserByUsername(username)
 *      从而实现登录。
 *   5. 登录成功/失败  默认都会进行跳转到设置的页面。重写可实现，对应的handler可以写入json ，从而实现处理ajax请求。
 *

 *
 *
 */





/**                          在FilterComparator 类中可以看见 常见过滤器的  【位置顺序】
 * 1.  内置过滤器的初始化：    在创建httpSecurity 对象时，FilterComparator 回对其排序。
 *
 * 2.  AutowireBeanFactoryObjectPostProcessor#ObjectPostProcessor () 手动注册一个new 的对象到springIOC容器中。
 *
 *
 *
 * 2.  JWT的整合
 *        1. 登录成功后，在 successLoginHandler 生成token 返回。 在 failLoginHandler 提供失败的响应信息。
 *        2. 非登录的接口的jwt认证   需要自己 实现OncePerRequestFilter 类，  该类注册时， 必须 放在UsernamePasswordAuthenticationFilter 之前。
 *        4. formLogin 的登录，直接使用UsernamePasswordAuthenticationFilter接口即可。
 *           使用json 的方式的登录，可以继承  AbstractAuthenticationProcessingFilter类，重写attemptAuthentication（）方法，
 *            将读取数据的方式变为 读取requestBody 的方式。
 *
 *  3. 图形验证码校验：
 *        1. 自定义实现一个Filter ,  如果 判断拦截登录请求， 判断验证码，如果不成功就直接 抛出认证异常。
 *        2. 一般使用DaoAuthenticationProvider对象来实现 登录，  在authenticate（）时，会进行各种check,
 *           所以我们一般实现自己的Provider ,重写一个check 方法，在里面加入验证码的逻辑。
 *
 *
 * 3. springSecurity 的 同意异常处理：   springSecurity的异常分类两大类：
 *        1. AuthenticationException  认证相关的异常               //  实现AuthenticationEntryPoint  来处理异常
 *        2. AccessDeniedException   访问受保护资源抛出的异常       //  实现AccessDeniedHandler  来处理异常。
 *
 * 4.  接口访问权限的：
 *       1. 在 WebSecurityConfigurerAdapter的实现类中进行配置。
 *       2. 使用@EnableGlobalMethodSecurity 开启注解方式，  有三种可以用的方法，默认均为false
 *
 *  5.  SecurityContext : 表示一个和当前线程有关的 安全信息 对象。
 *      SecurityContextHodler:  定义了一系列的静态方法，增删改和当前线程挂有关的SucurityContext对象。
 *
 *  6. Spring Security 以一个单 Filter（FilterChainProxy） 存在于整个过滤器链中，
 *       而这个 FilterChainProxy 实际内部代理着众多的 Spring Security Filter
 *
 *  7. 一般来说，springSecurity不同的Filter 都对应着  一个 SecurityConfigurer对象。
 *    webSecurity对象： 包含了我们自定义的信息 的信息，最终该对象 的信息会被封装成一个  filterChain。
 *
 *    springSercurity 收集了所有的SecurityConfigurer对象并放入    【webSecurity对象】中。
 *    在webSecurity对象  执行doBuild() 构建springSecurityFilterChain 会遍历   SecurityConfigurer集合，
 *    执行对应的方法。
 *
 *  8. GenericFilterBean 类： springWeb提供的 Filter,用于springMVC环境中的Filter
 *
 *  9. Spring提供了 一个GenericFilterBean 类的实现 【DelegatingFilterProxy过滤器】，
 *      代理过滤器  加入 javaWeb环境中的FilterChain;   而 springSecurityFilter  由代理过滤器执行。
 *
 *  10. 通过配置maximumSessions(1).sessionRegistry(sessionRegistry())   来保证同一时间只有一个用户，   JWT 不需要
 *      并且可以自定义一个 sessionRegistry 来设置session的保存位置。
 *      还需要一个注入HttpSessionEventPublisher 对象，因为springSecurity 中通过监听Session的销毁事件， 来清理session
 *
 *
 *
 *
 *
 *
 */















@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@AllArgsConstructor
@ComponentScan("com.albedo.java.common.security")
@Profile(CommonConstants.SPRING_PROFILE_JWT)
public class SecurityJwtConfiguration extends WebSecurityConfigurerAdapter {

	private final AuthenticationManagerBuilder authenticationManagerBuilder;
	private final ApplicationProperties applicationProperties;
	private final UserDetailsService userDetailsService;
	private final TokenProvider tokenProvider;
	private final CorsFilter corsFilter;
	private final ApplicationContext applicationContext;


	/**
	 * https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated
	 * Encoded password does not look like BCrypt
	 *
	 * @return PasswordEncoder
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}

	@PostConstruct
	public void init() {
		try {
			authenticationManagerBuilder
				.userDetailsService(userDetailsService)
				.passwordEncoder(passwordEncoder());
		} catch (Exception e) {
			throw new BeanInitializationException("Security configuration failed", e);
		}
	}

	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	@Override
	public void configure(WebSecurity web) throws Exception {

		web.ignoring()
			.antMatchers(HttpMethod.OPTIONS, "/**")
			.antMatchers("/i18n/**")
			.antMatchers("/content/**")
			.antMatchers("/statics/**")
			.antMatchers("/assets/**/*.{js,html}")
			.antMatchers("/test/**");
	}

	@Bean
	@ConditionalOnMissingBean
	public AuthenticationEntryPoint authenticationEntryPoint() {
		return new Http401UnauthorizedEntryPoint(applicationProperties);
	}


	@Override
	protected void configure(HttpSecurity http) throws Exception {

		// 搜寻匿名标记 url： @AnonymousAccess
		Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
		// 获取匿名标记
		Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);

		http.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
			.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
			.and()
			.csrf()
			.disable()
			.headers()
			.frameOptions()
			.disable()
			.and()
			.sessionManagement()
			.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
			.and()
			.authorizeRequests()
			// 自定义匿名访问所有url放行：允许匿名和带Token访问，细腻化到每个 Request 类型
			// GET
			.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
			// POST
			.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
			// PUT
			.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
			// PATCH
			.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
			// DELETE
			.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
			// 所有类型的接口都放行
			.antMatchers(null,anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
			.antMatchers(ArrayUtil.toArray(applicationProperties.getSecurity().getAuthorizePermitAll(), String.class)).permitAll()
			.antMatchers(ArrayUtil.toArray(applicationProperties.getSecurity().getAuthorize(), String.class)).authenticated()
			.and()
			.apply(securityConfigurerAdapter());

	}

	private JwtConfigurer securityConfigurerAdapter() {
		return new JwtConfigurer(tokenProvider, applicationProperties);
	}


	/**
	 *   获取handlerMapping 中所有的handleMethod 方法， 通过反射来判断是否有自定义的注解。
	 *   获取对应的url ,放入anonymousUrls集合中
	 * @param handlerMethodMap
	 * @return
	 */
	private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {

		Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
		Set<String> get = new HashSet<>();
		Set<String> post = new HashSet<>();
		Set<String> put = new HashSet<>();
		Set<String> patch = new HashSet<>();
		Set<String> delete = new HashSet<>();
		Set<String> all = new HashSet<>();
		for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
			HandlerMethod handlerMethod = infoEntry.getValue();
			AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
			if (null != anonymousAccess) {
				//@ReqestMaping注解中的method值会被转成一个set集合。
				/// TODO: 2020/7/8  测试 @requestMapping中的method[] 以及getmapping中的method
				List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
				RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
				switch (Objects.requireNonNull(request)){
					case GET:
						get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
						break;
					case POST:
						post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
						break;
					case PUT:
						put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
						break;
					case PATCH:
						patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
						break;
					case DELETE:
						delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
						break;
					default:
						all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
						break;
				}
			}
		}
		anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
		anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
		anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
		anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
		anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
		anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
		return anonymousUrls;
	}

}
